feat: webMCP registers the editor's manifest tools on the host page - #47
Merged
Merged
Conversation
…, validated exclude, page-level tool names
…the model context per lifecycle transition
…nding, one lazy-chunk table
…he model-context host list; retry after a failed chunk load
…t pin Re-sync embed-api.json to the served /embed/json (16 operations): getAnnotatedPage joins IframeActions, the tool subpaths and the WebMCP tools; the generator honors an object schema whose additionalProperties is a value schema (a map: Record<string, T> / z.record), the shape of the badges output. EDITOR_READY and DOCUMENT_LOADED come from the manifest events like PAGE_FOCUSED and SUBMISSION_SENT: internal-protocol.ts is gone, the EditorEvent union is drift-guarded for exact equality with the generated outbound events, and the root exports EditorReadyPayload / DocumentLoadedPayload. The lazy WebMCP chunk budget follows the extra operation. Changesets for both packages.
…nt included
The host-page WebMCP tools are the editor's own records from /embed/json (operations[].tool):
the simplepdf_embed_* name, description, snake_case input schema and behavior hints, generated
into webmcp-tools.ts and registered as-is, so a page gets one tool set whether the editor is
embedded or opened directly. The hand-kept TOOL_ANNOTATIONS table and the camelCase
tool-input-schemas.ts are gone. All 16 operations register (loadDocument included, like the
editor); the /tools, /ai-sdk and /tanstack-ai subpaths are unchanged.
The option is webMCP: { enabled: false } | { enabled: true; exclude?: MethodName[] } on
createEmbed and <EmbedPDF>; { enabled: false } and omitted are one state; exclude takes SDK
method names, validated at construction against the generated method-names.ts list.
Tool calls resolve with the editor's wire-shaped Result (what the record's description
promises); the annotated page render travels once, as an MCP image content block, with the
badges map in the text block. Lazy chunk budget follows the verbatim records.
…e needs no OPERATIONS table The opt-in chunk dropped the shared operations table (a second copy of every description and the error-code arrays, 43% of its download) now that each generated record names the operation it dispatches to; its budget follows (4.5 KB). A map node with a required list fails the generator instead of losing the list; the option validator builds its error only when it throws; the protocol header scopes is_agentic_tool to the tool registries; the README recommends withholding loadDocument alongside submit; the React remount key's two equivalence pairs are pinned.
…budgets The root grew 197 B gzip for the 16th operation, the manifest lifecycle events and the result-shape plumbing, leaving 68 B under its cap against the file's 0.5-1.5 KB convention; the caps follow the measured sizes here, where the growth is explained.
…from the hint set; scope the in-tab claim
An unknown key on the webMCP option throws at createEmbed, so { enabled: true, exlude: [...] }
can no longer read as nothing withheld. The generated annotation type lists the same hint names
the generator validates. The README and changeset state what stays in the tab: editing, until
a submit call sends the document through the tenant's submission flow.
…ustive switch A new operation now has to state at this site whether its result is text or a picture; the single-operation predicate let it take the text envelope unnoticed.
…tool call never posts; docs and stubs follow the 16-op contract The README and changeset no longer say the document reaches no SimplePDF storage until submit (a default embed uploads it to the configured storage at load); they state what is true: every operation runs in the browser, nothing the agent reads is computed server-side, and storage follows the account's configuration (SimplePDF-managed, S3, Azure Blob Storage or SharePoint) exactly as without WebMCP. A WebMCP call whose signal is already aborted rejects before reaching the editor. The changelog names the loadDocument contract change the manifest sync brought in; the shared actions stub and the tool router test cover getAnnotatedPage; the option-key guard is a real drift guard; the contract header names both root imports; the build-with-simplepdf skill documents webMCP.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
An in-browser agent (ChatGPT's browser, Chrome with WebMCP) discovers tools on the page it is looking at, not inside iframes. The SimplePDF editor registers its operations as WebMCP tools on its own page, so a site that embeds the editor exposes nothing to the agent. This PR lifts the editor's tools to the host page:
webMCPregisters, on the embedder'sdocument.modelContext, the very records the editor publishes in its manifest (https://simplepdf.com/embed/json,operations[].tool) and forwards each call to the editor over the existing bridge. One tool set whether the editor is embedded or opened directly; the SDK restates nothing the manifest publishes.It also carries the SDK regeneration against the current manifest (16 operations) and the lifecycle events derived from it.
Changes
createEmbed({ webMCP })and<EmbedPDF webMCP={...} />:{ enabled: true }registers every operation,{ enabled: true, exclude: ['submit', ...] }withholds some by SDK method name,{ enabled: false }and omitting the option are one state; a malformed value, an unknown key or a misspelled name throwsEmbedConfigErrorat constructionsimplepdf_embed_*name, description, snake_case input schema and behavior hints (readOnlyHint+untrustedContentHinton the readers,destructiveHinton the rest,openWorldHintwhere the editor fetches an agent-supplied URL); all 16 operations register,loadDocumentincluded, like the editor's own registration{ success, data | error }Result (isErroron failure);simplepdf_embed_get_annotated_pagecarries its PNG as animagecontent block, the badges map in the text block; the envelope is chosen per operation in an exhaustive switch, so a new operation has to declare text or image; a call whosesignalis already aborted rejects and never reaches the editorbooting, re-probes for a model context on later transitions, and lazy-loads the WebMCP module only then; tool names are page-level (the first embed owns a name, a second one is warned);dispose()unregisters everythinggetAnnotatedPage({ page })joinsIframeActions,useEmbed().actionsand the tool subpaths; the generator honors thebadgesmap shape (additionalPropertiesasRecord<string, T>/z.record)EDITOR_READYandDOCUMENT_LOADEDcome from the manifesteventslikePAGE_FOCUSEDandSUBMISSION_SENT;internal-protocol.tsis gone and theEditorEventunion is drift-guarded for exact equality with the generated outbound eventsloadDocumentis named in the recommendedexcludeloadDocument, exclude by method name, wire round trip in snake_case, image block and its fallback, an aborted call, readiness gate, dispose, name ownership, no-context and invalid-context diagnostics), the option validator (every malformed shape, an unknown key, an unknown name) and the React prop (registers, unregisters on unmount, the remount key's equivalence pairs); READMEs, three changesets (minor for both packages), API reports, bundle budgetsNotes
executeresolves with and defines no image channel; theimageblock pays off on runtimes that map results onto MCP'sCallToolResult, and the plain-JSON path sees the same bytes either way (the PNG travels once).EDITOR_READYpayload change on the editor side); a client-side cap on in-flight tool calls (a same-page script has the same power today); the root README's data-path sentences (healthcare and Copilot sections), revisited in a follow-up.minor. TheOutboundEventTypewidening (two members) is stated in its changeset.Review outcomes
WEBMCP_TOOLScarries the manifesttoolper operation, registered as-isresultShapekeeps the WebMCP path on the snake_case shape its tool descriptions promise{ enabled: true, exlude: [...] }throws instead of reading as nothing withheldimageblock + text block with page, size and badges; one arm per operation so a new one must pick its envelopewebMCPoption, one decoder{ enabled: false } | { enabled: true; exclude?: MethodName[] }, decoded once for the bridge, the module and the React keywireType; the opt-in download is 4.3 KB gzip against a 5 KB budgetadditionalPropertiesfailed the buildadditionalPropertieson an object withoutpropertiesemitsRecord<string, T>/z.record; a map withrequiredstill fails loudINTERNAL_PROTOCOLforEDITOR_READY/DOCUMENT_LOADEDOUTBOUND_EVENTS; theEditorEventunion is drift-guarded for exact equalityloadDocumentis named in the recommended exclude listexecutehonors the specification'ssignalbefore posting; a call aborted after it was posted still runs in the editor, which the code states (the wire has no cancel frame)